//*************************************************************************************
/** \file  ministroni054.c
 *  This file contains a program to run the "Ministrone" tensile tester interface. The 
 *  interface contains an ATMega8, an instrumentation amplifier, digital inputs, and a 
 *  USB to serial chip. The purpose of the program is to record data from a tensile 
 *  test so that an engineering stress-strain diagram can be plotted. 
 *
 *  Revisions:
 *    \li 06-18-2006 JRR Original program, an A/D test program
 *    \li 07-07-2006 JRR Accelerometer test V 0.1
 *    \li 09-10-2006 JRR Debugged current/min/max display, V 0.2
 *    \li 10-22-2006 JRR Packaged for distribution, V 0.3
 *    \li 01-11-2007 JRR Transformed into Minestroni interface program
 *    \li 03-17-2007 JRR Added millivolt conversions and counter reset
 *    \li 04-08-2007 JRR Added help screen
 *    \li 02-02-2010 JRR Changed to main/lib dir. tree, added ch. 0-7 options
 *    \li 01-10-2011 JRR Modified into a really low budget USB oscilloscope with data
 *                       taken in ISR and sent as a batch to be saved or plotted
 *    \li 01-19-2011 JRR Added rising and falling edge triggering; debugged stuff
 *
 *  License: This file is released under the Lesser GNU Public License. 
 */
//*************************************************************************************

#include <stdlib.h>							// Standard C/C++ library
#include <avr/io.h>							// Port I/O for SFR's
#include <avr/wdt.h>						// Watchdog timer header

#include "queue.h"							// Queue (ring buffer) template library
#include "avr_adc.h"						// Very simple A/D converter driver
#include "rs232int.h"						// Serial port with receive interrupts


//-------------------------------------------------------------------------------------
/// This is the size of the buffer which holds data points waiting to be sent
#define DATA_BUFFER_SIZE	200

/** This queue holds data points. It is written by an interrupt service 
 *  routine, and it is read by the main program as data is sent to the host
 *  via the serial port.
 */

#if DATA_BUFFER_SIZE < 255
	queue<uint16_t, uint8_t, DATA_BUFFER_SIZE> data_buffer;

	/// This is the number of data points which have been recorded so far.
	uint8_t points_saved;
#else
	queue<uint16_t, uint16_t, DATA_BUFFER_SIZE> data_buffer;

	/// This is the number of data points which have been recorded so far.
	uint16_t points_saved;
#endif

/// This is the number of times to read the A/D converter for each measurement
#define N_OVERSAMPLE		1				// Maximum allowable value = 31

/** This is the number of times a 1 or 0 reading must be repeated before the
 *  program is convinced that it has seen a pulse on the crank sensor rather
 *  than just some noise */
#define CRANK_RD_TOG		500

/// This define sets the port which has the crank sensor
#define CRANK_PORT			PIND

/// This define chooses the data direction register for the crank sensor port
#define CRANK_DDR			DDRD

/// This mask isolates the crank sensor input bit
#define CRANK_MASK			0x04

/// This is the separator character, written between data columns
#define SEPARATOR			'\t'


//-------------------------------------------------------------------------------------
/** This array contains numbers of milliseconds per point for each timebase setting.
 *  Based on 4 interrupts per millisecond, 100 points per screen, and 5 divisions per 
 *  screen:
 *  \li Mode  time/div   ms/pt  ISR's/pt
 *  \li  0    1 sec      50        200
 *  \li  1    500 ms     25        100
 *  \li  2    200 ms     10        40
 *  \li  3    100 ms     5         20
 *  \li  4    50 ms      2.5       10
 *  \li  5    20 ms      1         4
 *  \li  6    10 ms      0.5       2
 *  \li  7    5 ms       0.25      1
 */

uint8_t inters_per_reading[8] = {200, 100, 40, 20, 10, 4, 2, 1};

/// This counter is used to cause data to be taken at the correct time interval
uint8_t inter_counter;

/// This is the maximum value to which inter_counter counts
uint8_t max_inter_count;


/// This number is the channel on which we're going to take data
uint8_t data_channel;

/// This flag is true when the interrupt is supposed to be taking data
bool take_data_now = false;

/// This flag is set when beginning to take data and cleared when header is sent
bool send_a_header = false;

/// This 32-bit number holds the data checksum as it's being added up
uint32_t data_checksum = 0;


//-------------------------------------------------------------------------------------
/** This enumeration lists the possible trigger modes. Modes defined are as follows:
 *  \li 0 - Manual triggering; AVR takes data when asked to by serial character
 *  \li 1 - Rising edge triggering; data taken when voltage crosses up over threshold
 *  \li 2 - Falling edge triggering; data taken when voltage crosses downward
 */

enum trigger_t {TRIG_MANUAL, TRIG_POS_EDGE, TRIG_NEG_EDGE};

/// This integer holds the triggering mode.
trigger_t trig_mode = TRIG_MANUAL;

/// This is the trigger level for rising or falling edge triggering
uint16_t trig_level = 490;

/// This pointer points to the A/D converter used in the ISR
avr_adc* p_adc = NULL;


//-------------------------------------------------------------------------------------
/** This function reads a trigger level from the serial port. The trigger level must be
 *  a 4 digit decimal integer from 0000 to 1023. If a valid number is read, this 
 *  function sets the trigger level used by the ISR to begin recording data; if the
 *  next 4 digits don't contain a valid trigger level or there's a timeout, nothing is
 *  changed or set.
 *  @param uart A reference to the serial device with which we're communicating
 *  @param new_level A reference to the value for the trigger level, in the range 
 *                   [0..1023], which will be set only if valid data has been read
 */

#define TRIG_LEV_TIMEOUT	100000L

void get_trigger_level (base_text_serial& uart, uint16_t& new_level)
{
	uint16_t num_in = 0;					// Number being read in from serial device
	uint32_t tout_ctr = 0;					// Timeout counter for reading bytes
	uint8_t digit_num = 0;					// Count the digits being read in
	uint8_t char_in;						// Character read from serial port

	// Wait for characters and deal with them as they come in
	while (tout_ctr < TRIG_LEV_TIMEOUT)
	{
		if (uart.check_for_char ())			// If a character came in, read it
		{
			char_in = uart.getchar ();
			if (char_in > '9' || char_in < '0')
			{
				return;
			}

			num_in *= 10;					// Save the digit just received as the
			num_in += char_in - '0';		// least significant digit of the number

			// If we've received all 4 digits and the number is valid, modify the
			// value of the trigger level (it was passed as a reference)
			if ((++digit_num >= 4) && (num_in < 1024))
			{
				new_level = num_in;
				uart << "L " << new_level << endl;
				return;
			}
		}
		tout_ctr++;
	}

	// If we get here, not all 4 characters were read.  Just give up
	uart << "L " << new_level << endl;
}


//-------------------------------------------------------------------------------------
/** This function prints a simple help message.
 *  @param uart The serial device to which the message is printed
 */

void print_help_message (base_text_serial& uart)
{
	uart << PMS ("?: Ministrone v0.50 help") << endl;
	uart << PMS (" 0-7: Set data channel, 0 through 7") << endl;
	uart << PMS (" t:   Set timebase, 0 through 8") << endl;
	uart << PMS (" g:   Go (begin taking and sending data)") << endl;
	uart << PMS (" s:   Stop taking data") << endl;
	uart << PMS (" +:   Set rising edge trigger") << endl;
	uart << PMS (" -:   Set falling edge trigger") << endl;
	uart << PMS (" n:   Set no active trigger") << endl;
	uart << PMS (" l#:  Set trigger level to # (4 digits)") << endl;
	uart << PMS (" v:   send program version ID") << endl;
}


//=====================================================================================
/** This main function runs the tensile tester. After some very simple setup, it enters 
 *  a simple endless loop in which it checks for single-key user commands, then 
 *  responds to those commands appropriately. If an unrecognized command is given, a
 *  helpful message is returned. 
 */

int main (void)
{
	uint8_t char_in = 0;					// Character read from serial port

	#if DATA_BUFFER_SIZE < 255				// The number of points which have been
		uint8_t points_sent = 0;			// transmitted through the UART so far.
	#else									// When they've all been sent, a footer
		uint16_t points_sent = 0;			// will be sent with a checksum
	#endif

	MCUSR = 0;								// Disable watchdog timer during setup
	wdt_disable ();

	data_channel = 0;						// Read channel 0 by default
	points_saved = 0;						// We haven't saved any data yet
	take_data_now = false;					// Don't measure until asked to
	inter_counter = 0;						// Counter for data taking timing

	// Set the default value for time per measurement
	max_inter_count = inters_per_reading[0];

	rs232 uart (57600, 0);					// Set up the serial port

	// Set up the A/D converter driver and save a pointer to it
    avr_adc my_adc;							// Prepare the A/D converter
    p_adc = &my_adc;						// Set the pointer so ISR can find it

	// Set up Timer 1 to interrupt ? times every millisecond: Set CTC clear on output 
	// compare match mode; set clock select to IO clock / 8.  For 4 MHz that's one
	// clock tick every 2 us. 
	TCCR1A = 0;
	TCCR1B = (1 << WGM12) | (1 << CS11);
	OCR1A = (uint16_t)(125L * F_CPU / 8000000L);
	#ifdef TIMSK1
		TIMSK1 |= (1 << OCIE1A);
	#else
		TIMSK |= (1 << OCIE1A);
	#endif

	// Activate the watchdog timer to reset if the system is AWOL for 2 seconds
	wdt_enable (WDTO_2S);

	// Enable interrupts so that the serial port can receive characters and the timer
	// can go off to enable taking of data
	sei ();

	while (true)
	{
		// Check for a command character in the buffer; if so, read it and act upon it
		if (uart.check_for_char ())
		{
			char_in = uart.getchar ();			// Read the character

			switch (char_in)
			{
				case '0':						// A number from 0 to 7 means to get
				case '1':						// data from the given channel
				case '2':						// immediately and send it out the
				case '3':						// serial port
				case '4':
				case '5':
				case '6':
				case '7':
					uart << my_adc.read_oversampled ((char_in - '0'), N_OVERSAMPLE) 
						 << endl;
					break;

				case 'c':						// 'c' sets the data channel
					while (!(uart.check_for_char ()));
					char_in = uart.getchar () - '0';
					if (char_in < 8)			// Must be a valid timebase number
					{
						data_channel = char_in;
					}
					break;

				case 'g':						// 'g' means begin taking data
					if (take_data_now == false)
					{
						cli ();
						points_saved = 0;		// Make sure we start with an empty
						data_buffer.flush ();	// data buffer
						take_data_now = true;
						send_a_header = true;
						sei ();
					}
					break;

				case 'v':						// Initialize and/or information(?)
					uart << PMS ("Ministrone V0.54") << endl;
					break;

				case 'l':						// Change level for edge triggering
					get_trigger_level (uart, trig_level);
					break;

				case 's':						// Stop measurements
					if (take_data_now == true)
					{
						take_data_now = false;
					}
					break;

				case '+':						// Turn on rising edge triggering
					trig_mode = TRIG_POS_EDGE;
					break;

				case '-':						// Turn on falling edge triggering
					trig_mode = TRIG_NEG_EDGE;
					break;

				case 'n':						// Turn on off internal triggering
					trig_mode = TRIG_MANUAL;
					break;

				case 't':						// 't' sets the time per data point
					while (!(uart.check_for_char ()));
					char_in = uart.getchar () - '0';
					if (char_in < 8)			// Must be a valid timebase number
					{
						max_inter_count = inters_per_reading[char_in];
					}
					break;

				case '?':						// '?' is a plea for help
					print_help_message (uart);
					break;

				default:						// If character is not recognized,
					uart.putchar (char_in);		// give the luser some help
					uart << PMS ("WTF?") << endl;
					break;
				};
		}

		// If this is the first data in a bufferfull, send the header first
		if (send_a_header)
		{
			uart << "#" << DATA_BUFFER_SIZE << endl;
			send_a_header = false;
			points_sent = 0;
		}

		// If there's any data in the data queue, print it to the serial port 
		if (!(data_buffer.is_empty ()))
		{
			uint16_t temp_data = data_buffer.get ();
			data_checksum += temp_data;
			uart << temp_data << endl;
			points_sent++;
		}

		// If a full set of data has just been sent, send the checksum
		if (points_sent >= DATA_BUFFER_SIZE)
		{
			uart << "$" << (uint8_t)(data_checksum & 0xFF) << endl;
			points_sent = 0;
		}

		// Reset the watchdog timer so it doesn't cause the processor to reset
		wdt_reset ();
	}										// End main while loop

	return (0);
}


//-------------------------------------------------------------------------------------
// This interrupt service routine takes and saves data if data recording has been 
// activated. It uses the Timer 1 compare match vector; Timer 1 will have been set up 
// to control the rate at which the interrupt occurs. 

ISR (TIMER1_COMPA_vect)
{
	static uint16_t prev = 0;				// Save previous A/D reading for triggering
	uint16_t curr;							// Store current A/D reading here

	// First, count out the right number of interrupts so that data is taken at the
	// correct time interval
	if (++inter_counter >= max_inter_count)
	{
		inter_counter = 0;

		// Take a reading
		if ((trig_mode != TRIG_MANUAL) || take_data_now)
		{
			curr = p_adc->read_oversampled (data_channel, N_OVERSAMPLE);
		}

		// If not running, check for trigger condition
		if (take_data_now == false)
		{
			// If not taking data and rising edge triggered, check the input
			if ((trig_mode == TRIG_POS_EDGE) && (curr > trig_level) 
				&& (prev <= trig_level))
			{
				points_saved = 0;				// Make sure we start with an empty
				data_buffer.flush ();			// data buffer
				take_data_now = true;
				send_a_header = true;
			}

			if ((trig_mode == TRIG_NEG_EDGE) && (curr < trig_level) 
				&& (prev >= trig_level))
			{
				points_saved = 0;		// Make sure we start with an empty
				data_buffer.flush ();	// data buffer
				take_data_now = true;
				send_a_header = true;
			}	
		}

		// If data is currently being saved, put it into the buffer
		if (take_data_now)
		{
			data_buffer.put (curr);
			if (++points_saved >= DATA_BUFFER_SIZE)
			{
				take_data_now = false;
			}
		}

		// Save the current reading for comparison next time around
		prev = curr;
	}
}
